" REPL: Ruby Expression Printing Linker
"
" Simple wrapper around IRB via DRb
"
" Originally from: http://www-user.tu-chemnitz.de/~fifr/projekte/vinfruby.html
" Modifications by: dan@rapt.us
"
" ABOUT:
"
" REPL allows you to connect to an IRB session in an external shell window
" and execute ruby code from VIM in that IRB-session
"
" It outputs the results to a REPL scratch window, which can in turn be edited,
" forming The Great Loop of Code and/or Data.
"
" Default mappings:
"
" <Leader>rt            opens a new 'xterm' with an irb-session (not on Windows)
" <Leader>rf            evaluates the current file 
" <Leader>rl            evaluates the current line
" <Leader>rd            evaluates the 'def'-block the cursor is in
" <Leader>rc            evaluates the 'class'-block the cursor is in
" <Leader>rr            evaluates "reload" (set this to load whatever needs reloading)
"
" In visual-mode
" <Leader>r             evaluates the currently highlighted code
"
" To install, put this file and the file 'repl.rb' in the
" directory ~/.vim/plugin
"
" This requires VIM to be built with the Ruby extension.

if !exists('loaded_repl')
    let REPL_title = "__REPL__"
    let loaded_repl = 'yes'
    
    " Horizontally split window height setting
    if !exists('REPL_WinHeight')
        let REPL_WinHeight = 10
    endif
endif

function! s:REPL_Window_Quit()
    let bufnum = bufnr(g:REPL_title)
    if bufnum != -1
        " Kill the REPL buffer
        exe 'silent! bd ' . bufnum
    endif
endfunction

function! s:REPL_Window_Prepare()
    " Store the current window
    let s:REPL_focused_window = winnr()

    " If the window is open, jump to it
    let winnum = bufwinnr(g:REPL_title)
    if winnum != -1
        " Jump to the existing window
        if winnr() != winnum
            exe winnum . 'wincmd w'
        endif
        return
    endif

    " Open a horizontally split window
    let win_dir = 'botright'
    " Horizontal window height
    let win_size = g:REPL_WinHeight

    " If the temporary buffer already exists, then reuse it.
    " Otherwise create a new buffer
    let bufnum = bufnr(g:REPL_title)
    if bufnum == -1
        " Create a new buffer
        let wcmd = g:REPL_title
    else
        " Edit the existing buffer
        let wcmd = '+buffer' . bufnum
    endif

    " Create the taglist window
    exe 'silent! ' . win_dir . ' ' . win_size . 'split ' . wcmd
    silent! setlocal buftype=nofile
endfunction

function! s:REPL_Finish()
    exe s:REPL_focused_window . 'wincmd w'
endfunction

function! s:openterm()
    ruby <<EOF
    Kernel.fork { system("xterm -e 'irb -r ~/.vim/plugin/vinfruby.rb'") }
EOF
endfunction

function! s:evalline()
    ruby <<EOF
    REPL.evaluate(VIM::Editor.line, VIM::Editor.cursor.row)
EOF
endfunction

function! s:evalvisual()
    ruby <<EOF
    REPL.evaluate(VIM::Editor.selection_text, 
                      VIM::Editor.selection_begin.row)
EOF
endfunction

function! s:evalfile()
    ruby <<EOF
    REPL.evaluate(VIM::Editor.lines.join("\n"), 1)
EOF
    return ""
endfunction

function! s:evalreload()
    ruby <<EOF
    REPL.evaluate("reload", 1)
EOF
    return ""
endfunction

function! s:evaldef()
    ruby <<EOF
    REPL.evaluate_current_block("def")
EOF
endfunction

function! s:evalclass()
    ruby <<EOF
    REPL.evaluate_current_block("class")
EOF
endfunction

nnoremap <silent> <Plug>Ropenterm :<C-U> call <SID>openterm()<CR>
nnoremap <silent> <Plug>Revalfile :<C-U> call <SID>evalfile()<CR>
nnoremap <silent> <Plug>Revalline :<C-U> call <SID>evalline()<CR>
nnoremap <silent> <Plug>Revaldef :<C-U> call <SID>evaldef()<CR>
nnoremap <silent> <Plug>Revalclass :<C-U> call <SID>evalclass()<CR>
nnoremap <silent> <Plug>Revalreload :<C-U> call <SID>evalreload()<CR>
vnoremap <silent> <Plug>Revalvisual :<C-U> call <SID>evalvisual()<CR>
nnoremap <silent> <Plug>Rtest :<C-U> call <SID>test()<CR>
nnoremap <silent> <Plug>Rquit :<C-U> call <SID>REPL_Window_Quit()<CR>

nmap   <Leader>rt   <Plug>Ropenterm
nmap   <Leader>rf   <Plug>Revalfile 
nmap   <Leader>rl   <Plug>Revalline 
nmap   <Leader>rd   <Plug>Revaldef 
nmap   <Leader>rc   <Plug>Revalclass 
nmap   <Leader>rq   <Plug>Rquit
nmap   <Leader>rr   <Plug>Revalreload
vmap   <Leader>r    <Plug>Revalvisual

ruby <<EOF
require 'drb'

module REPL
  def self.append thing
    thing.inspect.split("\n").each do |line|
      add_line line
    end
  end

  def self.hr
    @hr ||= ("=" * 20) + ("-" * 20) + ("- " * 10)
    add_line @hr
  end

  def self.add_line x
    VIM.evaluate %Q{append(line('$'), "#{x.gsub('"', '\\"')}") }
  end

  def self.reconnect
    @@irb_server = DRbObject.new(nil, 'druby://127.0.0.1:33668')
  end

  def self.evaluate(line, line_no)
    first_try = true
    begin
      stuff = @@irb_server.evaluate(line, line_no)
      if false
        # Disabling REPL window for now
        VIM.evaluate "s:REPL_Window_Prepare()"
        append stuff
        VIM.evaluate "s:REPL_Finish()"
      else
        print "REPL returned #{stuff.class}"
      end
      return stuff
    rescue Exception => ex
      if first_try
        first_try = false
        reconnect
        retry
      else
        print "REPL error: #{$!}"
      end
    end
  end

  def self.evaluate_current_block(block_name)
    lines = VIM::Editor.lines
    srow, scol, erow, ecol = REPL.find_block(lines,
      VIM::Editor.cursor.row-1, VIM::Editor.cursor.col, block_name)
    unless ecol.nil?
      txt = [lines[srow][scol..-1]] + 
          lines[(srow+1)..(erow-1)] + 
          [lines[erow][0...ecol]]
      txt = txt.join("\n")
      REPL.evaluate(txt, srow + 1)
    end
  end

  def self.find_block(lines, row, col, block_name)
    start_row, start_col = find_start_of_block(lines, row, block_name)
    return false unless start_row

    end_row, end_col = find_end_of_block(lines, start_row, start_col)
    return false unless end_row

    return [start_row, start_col, end_row, end_col]
  end

  def self.find_start_of_block(lines, start_row, block_name)
    re = Regexp.new("\\b(?:#{block_name})\\b(?!.*\\b(?:#{block_name})\\b)")
    start_row.downto(0) do |i|
        m = re.match(lines[i])
        return [i, m.begin(0)] if m
    end
    return false
  end

  def self.find_end_of_block(lines, start_row, start_col)
    level = 0
    c = start_col
    for i in start_row...lines.size
        begin
            l = lines[i][c..-1]
            if m = /(^|;)\s*(while|until|loop|for|ensure|begin|rescue|def|class)\b/.match(l)
                level += 1
                c += m.begin(2) + m[2].size
            elsif m = /\bdo\b(\s*\|[^\|]*\|)?/.match(l)
                level += 1
                c += m.begin(0) + 2
            elsif m = /\bend\b/.match(l)
                level -= 1
                c += m.begin(0) + 3
            end

            if level == 0
                return [i, c]
            end
        end until m.nil?
        c = 0
    end

    return false
  end
end

module VIM
  module Editor
    class Position
      attr_reader :row, :col

      def self.from_vim(row_cmd, col_cmd)
        self.new(VIM.evaluate(row_cmd), VIM.evaluate(col_cmd))
      end

      def initialize(row, col)
        @row = row.to_i
        @col = col.to_i
      end
    end

    module Reg
      def self.[](regname)
        VIM.evaluate("@#{regname.to_s}")
      end

      def self.[]=(regname, value)
        VIM.evaluate("let @#{regname.to_s} = #{value}")
      end

      def self.method_missing(name, *args)
        name=name.to_s
        if name[-1] == ?=
          self.[]=(*args)
        else
          self.[](*args)
        end
      end
    end

    def self.cursor
      Position.from_vim('line(".")', 'col(".")')
    end

    def self.num_lines
      VIM::Buffer.current.count
    end

    def self.line(n = nil)
      n.nil? ? VIM::Buffer.current.line : VIM::Buffer.current[n]
    end

    def self.lines(s = 1, e = -1)
      e = num_lines + e + 1 if e < 0
      (s..e).map {|i| line(i)}
    end

    def self.mark(name)
      Position.from_vim(%Q{line("'#{name}")}, %Q{col("'#{name}")})
    end

    def self.selection_begin
      return mark("<")
    end

    def self.selection_end
      return mark(">")
    end

    def self.selection_text
      return Reg["*"]
    end
  end
end

EOF

